package game.game;

import util.Camera;

/**
 * Controls the camera following a target on the screen.
 * todo: add intelligent camera control, such as "leading" and manual restriction
 */
public class CameraController {
	public static class Settings {
		// maximum amount on each axis that the camera will lead the target
		public float maxHorizLead = 96.0f;
		public float maxVertLead = 64.0f;

		// speed at which the camera leads the target
		// the "lead value" ranges from -1 to 1
		// for each unit moved in a direction, the lead value is increased by the following amounts
		public float horizLeadSpeed = 0.01f;
		public float vertLeadSpeed = 0.01f;
	}

	public CameraController(Camera camera,
	                        int roomWidth, int roomHeight,
	                        Settings settings) {
		this.camera = camera;
		this.roomWidth = roomWidth;
		this.roomHeight = roomHeight;
		this.settings = new Settings();
		this.settings.maxHorizLead = settings.maxHorizLead;
		this.settings.maxVertLead = settings.maxVertLead;
		this.settings.horizLeadSpeed = settings.horizLeadSpeed;
		this.settings.vertLeadSpeed = settings.vertLeadSpeed;
	}

	public void setRoomSize(int roomWidth, int roomHeight) {
		this.roomWidth = roomWidth;
		this.roomHeight = roomHeight;
	}

	// todo: methods for horiz/vert boundaries

	// initializes by centering at the target position
	public void initTarget(int targetX, int targetY) {
		leadPositionX = lastTargetX = targetX;
		leadPositionY = lastTargetY = targetY;

		centerCameraOnPoint(leadPositionX, leadPositionY);
	}

	// updates the camera controller to follow the target
	public void updateTarget(int targetX, int targetY) {
		int dx = targetX - lastTargetX;
		int dy = targetY - lastTargetY;

		float horizLeadInc = (float) dx * settings.horizLeadSpeed;
		float vertLeadInc = (float) dy * settings.vertLeadSpeed;

		// update leads
		horizLead = computeNewLead(horizLead, horizLeadInc);
		vertLead = computeNewLead(vertLead, vertLeadInc);

		leadPositionX = targetX + Math.round(settings.maxHorizLead * horizLead);
		leadPositionY = targetY + Math.round(settings.maxVertLead * vertLead);

		lastTargetX = targetX;
		lastTargetY = targetY;

		centerCameraOnPoint(leadPositionX, leadPositionY);
	}

	private float computeNewLead(float currentLead, float d) {
		// we want the following behavior:
		// if the current lead is the opposite sign as d, the lead increases in d's direction at a steady rate
		// if it is the same sign, the lead increases slower as it approaches 1

		if (d == 0.0f) {
			return currentLead; // no change
		}

		boolean rev = (d < 0.0f);
		if (rev) {
			d = -d;
			currentLead = -currentLead;
		}

		// first, if the current lead is less than 0, increment up to at most 0
		if (currentLead < 0.0f) {
			currentLead += d;
			// check if we're still less than 0
			if (currentLead <= 0.0f) {
				return rev ? -currentLead : currentLead;
			}

			// we incremented past 0 - the remaining d is the amount past we went
			d = currentLead;
			currentLead = 0.0f;
		}

		// consider the function f(t) = 1/(t+1)^2 for x >= 0
		// the integral of f(t) from 0 to infinity is 1
		// consider an interval [0,T] to represents a time interval where a d of 1 is being applied
		// the area under the f between 0 and T is the current amount lead
		// as T approaches infinity, lead will approach 1

		// find the corresponding "t" for the current lead value
		// want to solve: int_0^T dt/(t+1)^2 = lead
		// we find that T = 1/(1 - lead) - 1
		float t = 1.0f / (1.0f - currentLead) - 1.0f;

		// now we increase t by d and re-integrate to find the new lead
		t += d;

		// int_0^T dt/(t+1)^2 = 1 - 1/(T+1)
		float newLead = 1.0f - 1.0f / (t + 1.0f);

		if (rev) {
			newLead = -newLead;
		}
		return newLead;
	}

	// centers the camera around the given point, constrained to the room boundaries
	private void centerCameraOnPoint(int x, int y) {
		int centerX, centerY;
		if (roomWidth < camera.getViewportWidth()) {
			centerX = roomWidth / 2;
		} else {
			centerX = x;
			int min = x - camera.getViewportWidth() / 2;
			int max = min + camera.getViewportWidth();
			if (min < 0) {
				centerX -= min;
			} else if (max > roomWidth) {
				centerX -= (max - roomWidth);
			}
		}
		if (roomHeight < camera.getViewportHeight()) {
			centerY = roomHeight / 2;
		} else {
			centerY = y;
			int min = y - camera.getViewportHeight() / 2;
			int max = min + camera.getViewportHeight();
			if (min < 0) {
				centerY -= min;
			} else if (max > roomHeight) {
				centerY -= (max - roomHeight);
			}
		}

		camera.set((float) centerX, (float) centerY, 1.0f);
	}

	private Camera camera;
	private int roomWidth;
	private int roomHeight;
	private Settings settings;

	// position of camera, including "leading"
	private int leadPositionX, leadPositionY;

	// last updated target
	private int lastTargetX, lastTargetY;

	// amount we are currently "leading" horizontally and vertically, from -1 to 1
	float horizLead, vertLead;
}
